package static

import (
	"fmt"
	"net/http"
	"os"
	"path"
	"path/filepath"
	"strings"
	"sync"
)

type FileServer struct {
	root         string
	indexes      bool
	cache        bool
	cacheControl string
	maxAge       int
	allowedExt   map[string]bool
	etags        map[string]string
	mu           sync.RWMutex
}

func NewFileServer(root string) *FileServer {
	return &FileServer{
		root:       root,
		indexes:    true,
		cache:      true,
		maxAge:     86400, // 1 day
		allowedExt: make(map[string]bool),
		etags:      make(map[string]string),
	}
}

func (fs *FileServer) SetIndexes(enabled bool) {
	fs.indexes = enabled
}

func (fs *FileServer) SetCache(enabled bool) {
	fs.cache = enabled
}

func (fs *FileServer) SetCacheControl(value string) {
	fs.cacheControl = value
}

func (fs *FileServer) SetMaxAge(seconds int) {
	fs.maxAge = seconds
}

func (fs *FileServer) AllowExt(exts ...string) {
	for _, ext := range exts {
		fs.allowedExt[ext] = true
	}
}

func (fs *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	urlPath := r.URL.Path
	if !strings.HasPrefix(urlPath, "/") {
		urlPath = "/" + urlPath
	}

	// 规范化路径
	urlPath = path.Clean(urlPath)

	// 防止目录遍历攻击
	if containsDotDot(urlPath) {
		http.Error(w, "Invalid URL path", http.StatusBadRequest)
		return
	}

	// 构建完整文件路径
	fullPath := filepath.Join(fs.root, filepath.FromSlash(urlPath))

	// 检查文件是否存在
	fi, err := os.Stat(fullPath)
	if err != nil {
		if os.IsNotExist(err) {
			http.NotFound(w, r)
			return
		}
		http.Error(w, "Internal Server Error", http.StatusInternalServerError)
		return
	}

	// 处理目录
	if fi.IsDir() {
		if !fs.indexes {
			http.Error(w, "Directory listing not allowed", http.StatusForbidden)
			return
		}
		fs.serveDirectory(w, r, fullPath)
		return
	}

	// 检查文件扩展名
	if len(fs.allowedExt) > 0 {
		ext := strings.ToLower(filepath.Ext(fullPath))
		if !fs.allowedExt[ext] {
			http.Error(w, "File type not allowed", http.StatusForbidden)
			return
		}
	}

	// 设置缓存控制
	if fs.cache {
		if fs.cacheControl != "" {
			w.Header().Set("Cache-Control", fs.cacheControl)
		} else {
			w.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d", fs.maxAge))
		}

		// 设置ETag
		etag := fs.getEtag(fullPath, fi)
		w.Header().Set("ETag", etag)

		// 处理条件请求
		if match := r.Header.Get("If-None-Match"); match != "" {
			if match == etag {
				w.WriteHeader(http.StatusNotModified)
				return
			}
		}
	}

	// 设置Content-Type
	w.Header().Set("Content-Type", getContentType(fullPath))

	// 提供文件服务
	http.ServeFile(w, r, fullPath)
}

func (fs *FileServer) serveDirectory(w http.ResponseWriter, r *http.Request, dirPath string) {
	f, err := os.Open(dirPath)
	if err != nil {
		http.Error(w, "Internal Server Error", http.StatusInternalServerError)
		return
	}
	defer f.Close()

	files, err := f.Readdir(-1)
	if err != nil {
		http.Error(w, "Internal Server Error", http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	fmt.Fprintf(w, "<pre>\n")
	for _, file := range files {
		name := file.Name()
		if file.IsDir() {
			name += "/"
		}
		fmt.Fprintf(w, "<a href=\"%s\">%s</a>\n", name, name)
	}
	fmt.Fprintf(w, "</pre>\n")
}

func (fs *FileServer) getEtag(filePath string, fi os.FileInfo) string {
	fs.mu.RLock()
	etag, exists := fs.etags[filePath]
	fs.mu.RUnlock()

	if !exists {
		etag = fmt.Sprintf(`"%x-%x"`, fi.ModTime().Unix(), fi.Size())
		fs.mu.Lock()
		fs.etags[filePath] = etag
		fs.mu.Unlock()
	}

	return etag
}

func containsDotDot(v string) bool {
	if !strings.Contains(v, "..") {
		return false
	}
	for _, ent := range strings.FieldsFunc(v, isSlashRune) {
		if ent == ".." {
			return true
		}
	}
	return false
}

func isSlashRune(r rune) bool { return r == '/' || r == '\\' }

func getContentType(file string) string {
	ext := strings.ToLower(filepath.Ext(file))
	switch ext {
	case ".html", ".htm":
		return "text/html; charset=utf-8"
	case ".css":
		return "text/css; charset=utf-8"
	case ".js":
		return "application/javascript"
	case ".json":
		return "application/json"
	case ".png":
		return "image/png"
	case ".jpg", ".jpeg":
		return "image/jpeg"
	case ".gif":
		return "image/gif"
	case ".svg":
		return "image/svg+xml"
	case ".xml":
		return "application/xml"
	case ".pdf":
		return "application/pdf"
	default:
		return "application/octet-stream"
	}
}
